Ruby on Rails為用Ruby程式語言寫的開源網頁框架,Rails的發明者DHH挑選了Ruby做為Rails的程式語言。Rails在2004年發布以後的短短的時間內就迅速獲得很多開發人員歡迎,這都歸功於MVC架構,以及Rails慣例優於設定的特性(使用Rails應用程式的開發者不用了解太多Knowhow,只需遵循著慣例即可開發網站)。直到其他MVC網頁框架出現以前,Ruby on Rails以第一個擁有MVC框架紅極一時。
由於Ruby為直譯語言的緣故,有速度比較慢的詬病,又加上不像Python 後期發展成多領域的語言。Ruby發展至今僅作為網頁、外掛、韌體用途,近幾年逐漸式微。不過我並不這麼覺得Ruby on Rails會那麼快的凋零,如人飲水;冷暖自知,只有寫過Ruby的人才知道Ruby的好,加上Ruby一直有廣大的社群,加上Rails至今仍不斷地進步。
在這個月的IT鐵人比賽進行的同時,DHH同時發表了Rails7,以及全新Javascript載入方式esbuild。明年的IT鐵人賽也想要用全新的Rails7介紹我的新專案。
以下為9月與Rails7的發布影片
我們在Rails以前,會先花時間介紹Ruby程式語言。Day1-Day17 會著重介紹Ruby程式語言,在Day18以後才會從畫面開始切入Rails應用程式。
在正式切入主題以前,首先想要跟讀者們介紹Ruby程式語言的特色。
Ruby發明的宗旨就是希望程式設計師能夠用人類的語言寫程式。比起一般的程式語言,Ruby的寫法更加風雅,舉凡下面的例子來的寫法都很風雅,也不失死板,漢漢老師自己也是看上這一點才決定入坑Rails。
1.day.from_now # 從現在開始往前推1天
Datetime.now.tomorrow # 明天
Order.first # 第一張訂單
Product.last # 最後一個商品
2.even? # 2是偶數嗎?
nil.nil? # nil是nil嗎?
由於大家對於Ruby的刻板印象就是寫法很奔放,就會誤會Ruby微弱型別的語言,但其實Ruby 是一個強型別的語言。我們拿Javascript的某例與Ruby 做對比。
在Javascript的世界中,不同型別的2個東西可以互加!而由於Javascript 弱型別的特性,使得開發過程中常會延伸出難以找錯的問題,因此後來衍伸了Typescript嚴格定義了型別。不過TypeScript最後也會被編譯成Javascript,仍不會改變JS為弱型別語言的事實。
null + 1 // 1
undefined + 1 // NaN
在Ruby的世界中,可不允許兩個不同型別的物件互加。舉例來說,當我打 1 + nil,Rails 會 告訴我不同型別的不能加起來。
1 + nil
Traceback (most recent call last):
TypeError (nil can't be coerced into Integer)
不過,在Ruby的世界中,下列的寫法是合法的。若太常使用型別的強制轉換,一樣會造成日後偵錯的困難。
漢漢老師就很常因此深受其困擾。
price = nil
1 + price.to_i #=> 1
題外話,寫程式久了發現,不是每一個會引發錯誤的地方都要寫Rescue/Exception,有時候該讓它壞的地方還是要讓它壞掉,才能夠告訴使用者不能這樣操作。
當網站上了正式站以後,專案還是會有一些方式,能夠將原本會跑壞掉的程式碼救回來,例如說跳轉到500錯誤狀態等等。
多重繼承會使物件複雜度上升,然而這些狀況在Ruby程式語言不會遇到。
Ruby為單一繼承的語言,若要實現多重繼承,可以使用mixin的方式來達成多模組的繼承,而我們會在Day14講解關於mixin的繼承方式。
Ruby 的世界裡面,除了block以外,所有的東西都是物件。 包括nil也是在NilClass 產生的物件。
Block為Ruby語言的一大特色。因為Block的緣故,使得Ruby容易發展出DSL語言,例如Rails 的routes, RSpec,以及Sinatra,都是因為Ruby特有的block 特性衍生出其DSL語言。由於這系列的文章的對象是針對初階工程師,因此不會講關於DSL的細節,讀者們若有興趣可以參閱這篇Toptal發表的文章
我們會在 Day8 正式介紹block。block是漢漢老師非常喜歡的主題,也是許多工程師拿來做面試或討論的議題。
在詳細的介紹Ruby 程式語言以前,先介紹Ruby 的一些概念和基本語法
我們要介紹的第一個用法是用來判斷類別的方法。首先我們先講is_a?, kind_of?。
is_a? 和 kind_of? 的用法一樣,都是判斷是哪一種類別。舉例來說,若A類別繼承陣列,則 A.is_a?(Array) 會回傳true。被include的mixin 也會被 is_a?判斷為true
我們舉一個實際的例子來作為使用is_a?的 demo,以下為簡單判斷型別,並依據型別來判斷使用何種結果的方法。
def analyse_data(data)
return unless data.is_a? Array || data.is_a? String
data.is_a?(Array) ? "用陣列的處理方法" : "用字串的處理方法"
end
analyse_data([1, 2, 3]) #=> "用陣列的處理方法"
另外,instance_of? 則不會判斷上層被繼承或mixin為true,舉例來說,如果A繼承了Array,擁有了陣列的特性,但instance_of? 不會判斷A為陣列
A.is_a? A # true
A.is_a? Array # true
A.instance_of? Array # false
以下用實例介紹基本的 || and && 的用法。
false || 1 #=> 1
false && 1 #=> false
nil || 1 #=> 1
nil && 1 #=> nil
true || 1 #=> true
true && 1 #=> 1
需注意,若回傳值的話,我們需要多留意使用&&或||回傳的結果為何。除此之外,邏輯運算子很好用,有些情況甚至可以代替if else。接著我們用第二個例子來介紹 || and && 的用法
def foo
'foo'
end
# nil 或 false
nil && foo #=> nil
nil || foo #=> "foo"
false && foo #=> false
false || foo #=> "foo"
# true 或存在 instance
true && foo #=> "foo"
true || foo #=> true
'abc' && foo #=> "foo"
'abc' || foo #=> "abc"
看完以上例子之後,我們來說明 || and && 的使用情境
⭐️||的使用情境:
我的房間的衣架上有掛毛巾、門口也又掛毛巾、櫃子旁也有掛毛巾,我的習慣是會優先拿衣架上的毛巾,沒有的話會去門口拿,門口再沒有的話我才會拿櫃子旁的毛巾。上述的情境用程式碼可以表示成
tower = from_hanger || from_door || from_cabinet
實際上會用的情境可以是在多元登入,當我們用帳號搜尋不到帳號,改用電子信箱搜尋,若用電子信箱搜尋不到則改成使用電話號碼之類
⭐️ &&的使用情境:
初會學分拿到了才能修中會,中會修完才能修高會。所以如果elementary_accounting 為沒有資料( nil),系統就被被擋修。
elementary_accounting && medium_accounting && advanced_accounting
實務上,漢漢老師比較少使用 blank?, empty? ,大部分的問題用nil?, present?就可以應付,這邊先不做介紹了。我們舉下列來說明,並且舉例以前,先假設parse_data 為某一種解析資料的方法,這裡強調的是nil?, present?的使用方式,所以我們先不理會parse_data的功能是在做什麼。
以下三種的寫法相等
# 反向的寫法語意較不好
unless instance.nil?
instance.parse_data
end
# 清楚明瞭
if instance.present?
instance.parse_data
end
# 清楚明瞭
instance.present? && instance.parse_data
接著我們討論上面三者用法的差別
# present? 只會回傳 true/false
'a'.present? #=> true
nil.present? #=> false
[].present? #=> false
''.present? #=> false
# presence 會將 false, [], nil, '' 轉為 nil,因此可以拿來回傳值。
false.presence #=> nil
[].presence #=> nil
nil.presence #=> nil
''.presence #=> nil
⭐️presence 於畫面上的應用:
➡️ 假設退貨單沒有填寫退貨原因,則不顯示。若用present?則會顯示false
= tag.div return_order.failed_reason.presence
➡️ 下列例子是使用 presence 調整css,若親自取貨按鈕不在,則將列印明細按鈕寫入margin-left: auto;。
我們會在Day19介紹margin: auto的用法。使用presence搭配&&判斷的在於若顯示!sub_order.can_pickup?為true,則class上面也會顯示ml-auto,反之如果!sub_order.can_pickup?為false,則會顯示nil,並且['btn btn-outline-primary mr-2', nil].compact為['btn btn-outline-primary mr-2'],再透過join(' ')會變為'btn btn-outline-primary mr-2'
- if sub_order.can_pickup?
= link_to '親自取貨', '#',
class: 'btn ml-auto btn-warning mr-2',
data: { confirm: "確定嗎?" }
= link_to '列印明細', '#',
class: ['btn btn-outline-primary mr-2', (!sub_order.can_pickup?.presence && 'ml-auto')].join(' ')
⭐️ presence_in 的用法為篩選陣列的值
'q'.presence_in %w(q w e) #=> "q"
'a'.presence_in %w(q w e) #=> nil
搭配||可以設定預設值
'a'.presence_in %w(q w e) || 'b' #=> b
& 在Ruby的用途很多,以下列出三種含有&符號的使用用法
Proc 與 Block ➡️ Day8介紹nil.name# Traceback (most recent call last):# NoMethodError (undefined method `name' for nil:NilClass)nil&.name#=> nil
我們也可以使用 try用法來寫,不過try表示法比較冗就是了。
nil.try(:name)#=> nilnil&.name#=> nil
有趣的是,javascript 後來衍伸了很像的寫法,表示法為?.
null.name// Uncaught TypeError: Cannot read property 'name' of nullnull?.name// undefined
⭐️ 與 && 不一樣,a && b的意思為,若a為不為nil, false,則回傳b值
[1,3] & [1,2]#=> [1][1,3] && [1,2]#=> [1, 2]
object_id在Ruby語言為重要概念,雖然沒那麼實用,但object_id可以用來解釋Ruby存取記憶體的位置。
a = "a"#=> "a"b = a#=> "a"b.object_id#=> 70126864284020a.object_id#=> 70126864284020a.object_id == b.object_id#=> true
利用object_id,我們可以發現symbol在Ruby程式語言中,佔著同個記憶體位置,而String不是。
# Symbol 在記憶體為同個位置:hello.object_id == :hello.object_id# String 在記憶體不同位置"hello".object_id == "hello".object_idfalse
在提到 symbol的同時,我們也講講freeze 的原理
a = "a".freeze(1..3).map { "a".object_id } #=> [70240285526560, 70240285526420, 70240285526400](1..3).map { a.object_id } #=> [70240172465500, 70240172465500, 70240172465500]
先看沒有被結凍的字串對應的object_id,我們發現記憶體位置不一樣。使用freeze過後,所指向的object_id相同,因此使用freeze佔去的記憶體會比較小,效能會比較高。
由上述介紹的freeze 和 symbol,我們可以發現freeze 的原理跟 symbol 很像!
雖然在後面的Rails版本似乎已經不用寫freeze方法了,但如果要面試Rails工程師的讀者們,記得要看freeze的概念。
Day2介紹了Ruby的概念以及基本用法。在Day3-7的四天裡面,會介紹其他關於Ruby的基本語法